package net.openadventure;

import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.PixelGrabber;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Vector;

import javax.imageio.ImageIO;

import net.openadventure.sprites.Sprite;

/**
 * Engine is the main driver of the game. It contains the majority of the
 * connecting logic. It is responsible for repainting the foreground, changing
 * the background image and relaying mouse activity to the Director. The engine
 * also loads all media resources (images, sounds, etc.). TODO: move media
 * loading into a separate dedicated class.
 * 
 * @author Paul
 * 
 */
public final class Engine implements Runnable {

	private final long delay;

	// These canvases are created externally from the Engine and plugged in
	// after.
	private final ForegroundCanvas foregroundCanvas;

	@SuppressWarnings("unused")
	private final BackgroundCanvas backgroundCanvas;

	private final Director director;

	private Thread animator = null;

	// A cache of Images
	private final HashMap<String, BufferedImage> imageCache = new HashMap<String, BufferedImage>();

	public Engine(ForegroundCanvas foregroundCanvas,
			BackgroundCanvas backgroundCanvas, int fps) {
		this.foregroundCanvas = foregroundCanvas;
		this.backgroundCanvas = backgroundCanvas;
		delay = 1000 / fps;

		director = new Director(this);
	}

	/**
	 * Starts redrawing the screen
	 */
	public void start() {
		animator = new Thread(this, "animator");
		animator.start();
	}

	/**
	 * Controls the animation speed and sends calls to the Director to update
	 * the scene
	 */
	public void run() {
		// Remember the starting time
		long tm = System.currentTimeMillis();
		while (Thread.currentThread() == animator) {

			// Update the scene
			director.updateScene(foregroundCanvas.getMouseCoordinateX(),
					foregroundCanvas.getMouseCoordinateY());

			// Delay for a while
			try {
				tm += delay;
				Thread.sleep(Math.max(delay, tm - System.currentTimeMillis()));
			} catch (InterruptedException e) {
				break;
			}
		}
	}

	/**
	 * Updates the ScreenElements being painted by the ForegroundCanvas
	 * 
	 * @param sprites
	 *            a List of Sprites to convert and send to the ForegroundCanvas
	 * @return the previous mouse state of the ForegroundCanvas
	 */
	public MouseButtonState updateForeground(
			LinkedHashMap<String, Sprite> sprites) {
		// Convert Sprites to ScreenElements
		Vector<ScreenElement> elements = new Vector<ScreenElement>();

		Sprite[] spriteArray = new Sprite[sprites.size()];
		sprites.values().toArray(spriteArray);

		for (int i = 0; i < spriteArray.length; i++) {
			ScreenElement newElement = convertSpriteToScreenElement(spriteArray[i]);

			if (newElement != null) {
				elements.add(newElement);
			}
		}

		return foregroundCanvas.update(elements);
	}

	/**
	 * Sets the Image to display on the BackgroundCanvas
	 * 
	 * @param path
	 *            the relative path to the image file
	 */
	public void setBackgroundImage(String path) {
		backgroundCanvas.setImage(loadImage(path));
	}

	/**
	 * TODO: temp method remove later
	 * 
	 * @return foregroundcanvas
	 */
	public ForegroundCanvas getForegroundCanvas() {
		return foregroundCanvas;
	}

	/**
	 * Converts a Sprite to a ScreenElement. A screen element contains a loaded
	 * image with an (x,y) coordinate location. This method also applies all
	 * needed flips and resizing.
	 * 
	 * @param sprite
	 *            the source
	 * @return the new ScreenElement
	 */
	private ScreenElement convertSpriteToScreenElement(Sprite sprite) {
		BufferedImage image = loadImage(sprite.getImagePath());

		image = resizeImage(image, sprite.getWidth(), sprite.getHeight());

		// Apply the flips here
		if (sprite.getFlipHorizontal()) {
			image = flipImageHorizontal(image);
		}
		if (sprite.getFlipVertical()) {
			image = flipImageVertical(image);
		}

		return new ScreenElement(image, sprite.getX(), sprite.getY());
	}

	/**
	 * Loads a BufferedImage from a relative path string
	 * 
	 * @param path
	 *            the relative path to the image file
	 * @return the loaded Image
	 */
	private BufferedImage loadImage(String path) {

		// Try to get the image from the cache
		BufferedImage cachedImage = imageCache.get(path);

		// If the image is not cached, try loading it
		if (cachedImage == null) {

			BufferedImage newImage;

			// Load the new image
			try {
				newImage = ImageIO.read(new File(path));
			} catch (IOException e) {
				// Image failed to load, return null
				System.err.println("Image not found at: " + path);
				return null;
			}

			// Cache the new image
			imageCache.put(path, newImage);

			return newImage;

		} else {

			return cachedImage;

		}
	}

	/**
	 * Resizes a BufferedImage object. The resized BufferedImage is a copy of
	 * the source BufferedImage.
	 * 
	 * @param image
	 *            the source BufferedImage
	 * @param width
	 *            the new width
	 * @param height
	 *            the new height
	 * @return the new resized Image
	 */
	private BufferedImage resizeImage(BufferedImage image, int width, int height) {

		// If the new dimensions match the old dimensions, just return the image
		if (width == image.getWidth(null) && height == image.getHeight(null)) {
			return image;
		}

		// Check for valid dimensions. If invalid, just return the original
		// image dimensions
		if (width < 1) {
			width = image.getWidth(null);
		}
		if (height < 1) {
			height = image.getHeight(null);
		}

		// This will take the original image and create a new image based on
		// these new values.

		double widthScaleFactor = (double) width
				/ (double) image.getWidth(null);
		double heightScaleFactor = (double) height
				/ (double) image.getHeight(null);

		// Re-scale the image
		AffineTransform xform = AffineTransform.getScaleInstance(
				widthScaleFactor, heightScaleFactor);

		AffineTransformOp afOP = new AffineTransformOp(xform,
				AffineTransformOp.TYPE_BICUBIC);

		// Do not use setImage() we don't want to change the preImage
		return afOP.filter(image, null);

	}

	/**
	 * Flips an BufferedImage horizontally. The returned BufferedImage is a copy
	 * of the source Image.
	 * 
	 * @param image
	 *            the source BufferedImage
	 * @return the new flipped Image
	 */
	private BufferedImage flipImageHorizontal(BufferedImage image) {
		AffineTransform tx = AffineTransform.getScaleInstance(-1, 1);
		tx.translate(-image.getWidth(null), 0);
		AffineTransformOp op = new AffineTransformOp(tx,
				AffineTransformOp.TYPE_BICUBIC);
		return op.filter(image, null);
	}

	/**
	 * Flips a BufferedImage vertically. The returned BufferedImage is a copy of
	 * the source BufferedImage.
	 * 
	 * @param image
	 *            the source BufferedImage
	 * @return the new flipped BufferedImage
	 */
	private BufferedImage flipImageVertical(BufferedImage image) {
		AffineTransform tx = AffineTransform.getScaleInstance(1, -1);
		tx.translate(0, -image.getHeight(null));
		AffineTransformOp op = new AffineTransformOp(tx,
				AffineTransformOp.TYPE_BICUBIC);
		return op.filter(image, null);
	}

	// /**
	// * Converts a BufferedImage to a BufferedImage
	// *
	// * @param image
	// * the source BufferedImage
	// * @return the new bufferedImage
	// */
	// private BufferedImage convertImageToBufferedImage(Image image) {
	// // Create a BufferedImage of the correct size that supports
	// // transparency.
	// BufferedImage bImage = new BufferedImage(image.getWidth(null),
	// image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
	// Graphics2D bImageGraphics = bImage.createGraphics();
	// // Paint the image on the new BufferedImage
	// bImageGraphics.drawImage(image, 0, 0, null);
	// return bImage;
	// }

	/**
	 * Checks if a set of screen coordinates lies on top of the sprite's image.
	 * The method will return true if the coordinate point lies on top of a
	 * non-transparent pixel of this sprite's image.
	 * 
	 * @param hitCoordinateX
	 *            the x-coordinate, in pixels
	 * @param hitCoordinateY
	 *            the y-coordinate, in pixels
	 * @return true - the coordinate point lies on top of the image. false - the
	 *         coordinate point does not lie on top of the image.
	 */
	public boolean checkHit(Sprite sprite, int hitCoordinateX,
			int hitCoordinateY) {

		// Converting the Sprite to a ScreenElement will automatically load the
		// image
		// and perform the necessary flips and resizing.
		ScreenElement screenElement = convertSpriteToScreenElement(sprite);

		// Check if the click was over the Sprite
		if (hitCoordinateX >= sprite.getX()
				&& hitCoordinateX < (sprite.getX() + sprite.getWidth())
				&& hitCoordinateY >= sprite.getY()
				&& hitCoordinateY < (sprite.getY() + sprite.getHeight())) {

			// Translate the hitCoordinates to the Sprite's local coordinates
			int localHitCoordinateX = hitCoordinateX - sprite.getX();
			int localHitCoordinateY = hitCoordinateY - sprite.getY();

			// Create the pixel grabber that will get the single target pixel
			PixelGrabber pixelGrabber = new PixelGrabber(screenElement
					.getImage(), localHitCoordinateX, localHitCoordinateY, 1,
					1, true);

			try {
				pixelGrabber.grabPixels();
			} catch (Exception e) {
				return false;
			}

			// get the palette of the image
			ColorModel colorModel = pixelGrabber.getColorModel();

			// get the pixels
			int[] pixels = (int[]) pixelGrabber.getPixels();

			if (pixels != null && pixels.length > 0) {
				// return true if the hit landed on a non-transparent pixel
				if (colorModel.getAlpha(pixels[0]) != 0) {
					return true;
				}
			}
		}
		return false;
	}

}
